状態を持つ React Component を TDD で実装する
from Testing React with Testing Library
カルーセル コンポーネントを実装する
カルーセルは「どのスライドを現在表示しているか」を状態値として持つ必要がある
Step 1: 定義
テストを書く
code:Carousel.test.tsx
describe("Carousel", () => {
it("renders a <div>", () => {
render(<Carousel />);
expect(screen.getByTestId("carousel")).toBeInTheDocument();
});
});
テストをパスする最低限の実装を書く
code:Carousel.tsx
const Carousel = () => <div data-testid="carousel" />;
export default Carousel;
Step 2: スライドのリストを受け取り、そのうちの 1 つを表示する
コンポーネントを実装する際にはコア機能から始めると良い
テストを書く
code:Carousel.test.tsx
const slides = [
{
imgUrl: "https://example.com/slide1.png",
description: "Slide 1",
attribution: "Uno Pizzeria",
},
{
imgUrl: "https://example.com/slide2.png",
description: "Slide 2",
attribution: "Dos Equis",
},
{
imgUrl: "https://example.com/slide3.png",
description: "Slide 3",
attribution: "Three Amigos",
},
];
it("renders the first slide by default", () => {
render(<Carousel slides={slides} />);
const img = screen.getByRole("img");
expect(img).toHaveAttribute("src", slides0.imgUrl);
});
テストをパスする最低限の実装を書く
code:Carousel.tsx
type Slide = {
imgUrl?: string;
description?: ReactNode;
attribution?: ReactNode;
};
const Carousel = ({ slides }: { slides: Slide[] }) => {
return (
<div data-testid="carousel">
<CarouselSlide {...slides?.0} />
</div>
);
};
CarouselSlide の実装は 入れ子になった React Component を TDD で実装する 参考
Step 3: Next ボタンをクリックしたときにスライドを進める
ここから状態値とユーザの インタラクション 部分の実装が必要となる
warning.icon Enzyme とは異なり、Testing Library は内部状態に直接アクセスできない
コンポーネントの内部動作は ブラックボックス として扱われる
したがって、コンポーネントの状態をテストするアプローチは以下のようになる
1. コンポーネントをレンダリングする(render)
2. ユーザの インタラクション をシミュレートする: Testing Library#66d179c675d04f000026ddc9
3. コンポーネントの DOM 出力が期待通りに変化するか確認する
テストを書く
code:Carousel.test.tsx
it("renders the slide when the Next button is clicked", async () => {
render(<Carousel slides={slides} />);
const img = screen.getByRole("img");
const nextButton = screen.getByTestId("next-button");
const user = userEvent.setup();
await user.click(nextButton);
expect(img).toHaveAttribute("src", slides1.imgUrl);
await user.click(nextButton);
expect(img).toHaveAttribute("src", slides2.imgUrl);
await user.click(nextButton);
expect(img).toHaveAttribute("src", slides0.imgUrl);
});
Testing Library でシミュレートされたユーザイベントはすべて非同期関数
await で待機することで、イベントが完全に解決されてテストが続行される前に DOM が更新される
typescript-eslint の no-floating-promises ルールで防げる
https://typescript-eslint.io/rules/no-floating-promises/
テストをパスする最低限の実装を書く
code:Carousel.tsx
const Carousel = ({ slides }: { slides?: Slide[] }) => {
const slideIndex, setSlideIndex = useState(0);
return (
<div data-testid="carousel">
<CarouselSlide {...slides?.slideIndex} />
<CarouselButton
data-testid="next-button"
onClick={() => {
if (!slides) return;
setSlideIndex((i) => (i + 1) % slides.length);
}}
Next
</CarouselButton>
</div>
);
};
Step 4: Prev ボタンをクリックしたときにスライドを戻す
テストを書く
code:Carousel.test.tsx
it("reverses the slide when the Prev button is clicked", async () => {
render(<Carousel slides={slides} />);
const img = screen.getByRole("img");
const nextButton = screen.getByTestId("prev-button");
const user = userEvent.setup();
await user.click(nextButton);
expect(img).toHaveAttribute("src", slides2.imgUrl);
await user.click(nextButton);
expect(img).toHaveAttribute("src", slides1.imgUrl);
await user.click(nextButton);
expect(img).toHaveAttribute("src", slides0.imgUrl);
});
テストをパスする最低限の実装を書く
code:Carousel.tsx
const Carousel = ({ slides }: { slides?: Slide[] }) => {
const slideIndex, setSlideIndex = useState(0);
return (
<div data-testid="carousel">
<CarouselSlide {...slides?.slideIndex} />
<CarouselButton
data-testid="prev-button"
onClick={() => {
if (!slides) return;
setSlideIndex((i) => (i + slides.length - 1) % slides.length);
}}
Prev
</CarouselButton>
<CarouselButton
data-testid="next-button"
onClick={() => {
if (!slides) return;
setSlideIndex((i) => (i + 1) % slides.length);
}}
Next
</CarouselButton>
</div>
);
};
#React #TDD